package uniManager;


import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import people.Person;
import people.Person.Name;
import people.student.Student;
import permission.RoleManager;
import permission.Roleable;
import permission.role.AdministratorRole;
import permission.role.CourseLeaderRole;
import transformer.courseTransformer.CourseStringTransformer;
import transformer.studentTransformer.StudentStringTransformer;
import course.AbstractCourseType;
import course.Course;
import course.requirements.RequirementManager;
import evaluation.CourseEvaluation;
import evaluation.CourseTypeEvaluation;

public enum UniManager {
	// provide singleton access to the manager
	INSTANCE;

	private static Set<Course> courses = new HashSet<Course>();
	private static final Person ADMIN = new Person(new Name("admin", "admin"));
	static {
		ADMIN.addRole(new AdministratorRole());
	}
	
	public List<Course> getCoursesForStudent(Student s){
		List<Course> courses = new LinkedList<Course>();
		
		for(Course c : UniManager.courses){
			if(c.getRegisteredStudents(ADMIN).contains(s)){
				courses.add(c);
			}
		}
		
		return courses;
	}
	
	public List<AbstractCourseType> getTypesForStudent(Student s, Course c){
		List<AbstractCourseType> types = new LinkedList<AbstractCourseType>();
		
		for(AbstractCourseType t : c.getTypes()){
			if(t.getRegisteredStudents(ADMIN).contains(s)){
				types.add(t);
			}
		}
		
		return types;
	}
	
	public String getStudentsForCourse(String courseNumber, Roleable person) {
		if(courseNumber == null || courseNumber.isEmpty()) {
			throw new IllegalArgumentException("Parameter 'courseNumber' is not valid!");
		}
		
		Course course = getCourseById(courseNumber);
		Iterable<Student> students = course.getRegisteredStudents(person);
		if(students == null) {
			return "Information not visible for role";
		}
		
		StudentStringTransformer transformer = new StudentStringTransformer();
		return transformer.transform(students);
	}
	
	public Collection<Course> getAllCourses() {
		Collection<Course> courses = new HashSet<Course>();
		for(Course c : UniManager.courses) {
			courses.add(c);
		}
		return courses;
	}
	
	public String getAllCoursesAsString() {
		CourseStringTransformer transformer = new CourseStringTransformer();
		return transformer.transform(courses);
	}
	
	public int getCourseListSize() {
		return courses.size();
	}
	
	private Course getCourseById(String id) {
		for(Course course : courses) {
			if(course.getCourseID().equals(id)) {
				return course;
			}
		}
		return null;
	}
	
	public void addCourse(Course c) {
		if(c == null) {
			throw new IllegalArgumentException("parameter 'c' can not be null!");
		}
		courses.add(c);
	}

	
	/**
	 * this methods unregisters a student from a course.
	 * 
	 * @param c
	 * @param s
	 * @throws Exception when the unregister-period is over or when the student is not registered for this course
	 */
	public void unregisterStudent(Course c, Student s) throws Exception{
		Date now = new Date();
		
		// we do not need to check for registerStart here
		// as there can not be any students in the course
		// before this date anyway.
		
		if(c.getUnregisterEnd().before(now))
			throw new Exception("Unregister-period is over! (Ended: "+c.getUnregisterEnd()+")");
		
		if(!c.getRegisteredStudents(ADMIN).remove(s))
			throw new Exception("Student "+s+" is not registered for this course!");
	}

	/**
	 * register a student to a course
	 * 
	 * @param c
	 * @param s
	 * @throws Exception when the student already exists or the register-period is already over or did not start yet
	 */
	public void registerStudent(Course c, Student s) throws Exception{
		this.registerStudent(c, s, null);
	}

	/**
	 * register a student to a course
	 * 
	 * @param c
	 * @param s
	 * @param options you choose from the list you got from the registerOptions-method from the CourseTypes of the course c
	 * @throws Exception when the student already exists or the register-period is already over or did not start yet
	 */
	public void registerStudent(Course c, Student s, HashMap<AbstractCourseType, Integer> options) throws Exception{
		// check if student fulfills requirements for course

		if(!RequirementManager.INSTANCE.isStudentFulfillingRequirements(s, c)) {
			throw new Exception("Student " + s.getMatrikelNr(ADMIN) + " does not fulfill the requirements to register for course " + c.getCourseName());
		}
		
		Date now = new Date();	
		
		if(c.getRegisterStart().after(now) || c.getRegisterEnd().before(now))
			throw new Exception("Not able to register. Only from " + c.getRegisterStart() + " to " + c.getRegisterEnd());
		
		registerStudentInternal(c, s, options);
	}
	
	public void registerStudentNoChecks(Course c, Student s, Roleable role) throws Exception {
		if(!RoleManager.INSTANCE.isAdministrator(role) && !RoleManager.INSTANCE.isCourseLeader(c, role)) {
			throw new IllegalArgumentException("role is not allowed to register student without checks");
		}
		
		registerStudentInternal(c, s, null);
	}
	
	private void registerStudentInternal(Course c, Student s, HashMap<AbstractCourseType, Integer> options) throws Exception {
		
		if(c.getRegisteredStudents(ADMIN).contains(s))
			throw new Exception("Student " + s + " already registered!");
		
		// add course leader access role for this course
		s.addAccessRole(new CourseLeaderRole(c));
		
		// report the registration-options to all the course-types
		
		// if no options were given we just make an empty map
		if(options == null){
			options = new HashMap<AbstractCourseType, Integer>();
		}
		
		// we need this to make sure that every coursetype's
		// finalizeRegistration-method is called
		List<AbstractCourseType> types = new ArrayList<AbstractCourseType>(c.getTypes());
		
		// iterate trough the given options and report to the type.
		for (AbstractCourseType type : options.keySet()){
		
			if(types.contains(type)){
				if(!type.finalizeRegistration(s, options.get(type))){
					throw new Exception("Registration Failed (in registerStudent): "+type+" with option "+options.get(type)+" raised an error");
				}
				
				types.remove(type);
			}
		}
		
		// if there are types left that we did not report to 
		for(AbstractCourseType type : types){
			// if there is more than 1 option, we have a problem
			if(type.registrationOptions(s).size() > 1)
				throw new Exception("no option for type "+type);
		}
		
		c.getRegisteredStudents(ADMIN).add(s);
	}
	
	/**
	 * Adds an additional CourseType to the Course and linkes the Reference to the CourseType
	 * @param c
	 * @param ac
	 * @throws Exception
	 */
	public void addCourseType(Course c, AbstractCourseType ac) throws Exception{
		if(c != null && ac != null){
			c.addTypes(ac);
			ac.setCourse(c);
			ac.addAccessRole(new CourseLeaderRole(c));
		}
		else
			throw new Exception("Illegal Arguments used");
	}

	/**
	 * Adds an Evaluation to a Student
	 * @param ac
	 * @param s
	 * @param points this can be a grade, a percentage, ... depending on the CourseType
	 */
	public void evaluateStudent(AbstractCourseType ac, Student s, int points) throws Exception{
		if(ac.getCourse().getRegisteredStudents(ADMIN).contains(s)){
			boolean suc = s.addCourseTypeEvaluation(new CourseTypeEvaluation(ac,points));
			if(!suc)
				throw new Exception("Not able to evaluate Student "+ s);
		}
		else
			throw new Exception("Student "+s+" not in the Course "+ac.getCourse().getCourseID()+" / "+ac.getCourse().getCourseName());
	}
	
	public void evaluateStudent(Roleable role, Course course, Student student, int points) {
		if(!course.getRegisteredStudents(role).contains(student)) {
			return;
		}
		
		CourseEvaluation eval = new CourseEvaluation(course, points);
		boolean success = student.addCourseEvaluation(eval, role);
		if(!success) {
			throw new IllegalArgumentException("role is not allowed to add evaluation");
		}
	}

	/**
	 * "Removes" the Course by changing it's flag
	 * 
	 * @param cid Course ID
	 * @throws Exception
	 */
	public void removeCourse(String cid) throws Exception{
		Course c = getCourseById(cid);
		if(c == null)
			throw new Exception("Course "+cid+" not found");
		removeCourse(c);
	}
	
	/**
	 * change the state of a course to "removed"
	 * 
	 * @param c the course to change
	 */
	public void removeCourse(Course c) throws Exception{
		if(c == null)
			throw new Exception("Course not found!");
		if(courses.contains(c)){
			if(c.isRemoved()){
				throw new Exception("Course "+c.getCourseID()+" already removed!");
			}
			else
				c.setRemoved(true);
		}
		else{
			throw new Exception("Course "+c.getCourseID()+" not found!");
		}
	}

}